iT邦幫忙

2023 iThome 鐵人賽

DAY 23
0
Mobile Development

用 SwiftUI 魔法變出 Leetcode 刷題知識學習 App!系列 第 23

Day 23: SwiftUI 紀錄收藏的 LeetCode 題目:UserDefaults 和 @AppStorage

  • 分享至 

  • xImage
  •  

LeetCode 題目隨著我們一題一題實作過後,我們需要知道到底做過了多少題目,將它收藏起來,不想要打開 App 的時候發現怎麼跟剛下載的一樣,一點變化都沒有,也不記得上次做過哪幾題,本次主題就是要利用 SwiftUI 的 UserDefault@AppStorage 來幫助記憶實作及收藏過的 LeetCode 題目。

UserDefaults 介紹

這是 iOS 系統用來簡單數據持久化的解決方案,也就是說它會以鍵值對(Key-Value)的方式儲存在本機,建議儲存的是小量的資料,像是用戶的設置、偏好,應用程式的狀態訊息等等。

也就是說如果我們有把資料存在 UserDefaults,當 App 開啟→結束→再開啟時,資料可以再次被讀取,而如果沒有存,再開啟時就會消失,恢復預設。

UserDefaults 可以用來儲存以下基本類型:

  • String
  • Int
  • Double
  • Float
  • Bool
  • URL
  • Data
  • Date

比較多元的資料,可以將自定義的類型轉換成 Data 後再存,或者實現 Codable protocol 以便於物件之間的序列化和反序列化,然後再存。

// 實例化 UserDefaults
let defaults = UserDefaults.standard

// 儲存資料
defaults.set("John Doe", forKey: "userName")

// 讀取資料
if let name = defaults.string(forKey: "userName") {
    print("User Name: \(name)")
}

以上為儲存以及讀取的 Swift 範例程式碼。

@AppStorage 介紹

@AppStorage是 SwiftUI 專屬用來儲存資料持久化的一個簡單的屬性包裝器,它只能在 SwiftUI 裡面的 View 使用,而且由於 @AppStorage 是一個綁定,它可以自動更新 View,從而使 View 能夠反映儲存資料的最新更改。而它的底層也是保存到 UserDefaults,只是有了它我們不用再調用落落長的語法。

SwiftUI 使用方式如下:

@AppStorage("userName") var userName: String = "John Doe"

var body: some View {
    VStack {
        Text("Welcome, \(userName)!")
        
        TextField("Enter your name", text: $userName)
            .padding()
    }
}

一樣必須要注意的是 “userName” 需要是唯一 Key 值,它綁定於變數 userName,所以變數更新它也會跟著更新。如果它不是唯一 Key值,那就要注意其他地方如果也使用可能造成數值被修改,就要小心,但有了它讓 SwiftUI 在保存簡單數值時更加容易。

這兩個儲存方式都要注意不要存太大量的資料,否則開啟 App 會變得十分緩慢,因為它會把資料讀取後存在記憶體,而本文只簡單存 LeetCode 題目編號,紀錄實作過的題目。

LeetCode 題目資料儲存

首先無疑是先定義 LeetCode 題目資料物件,Identifiable 前面文章提過是為了要給 SwiftUI List 獨立識別 ID,這邊為了持久化資料,也就是存到 UserDefaults ,所以需要增加 Codable 的 protocol。

struct LeetCodeProblem: Identifiable, Codable {
    var id = UUID()
    var title: String
    var isFavorited = false
}

接下來我們定義一個 Manager 來管理儲存的資料操作。LeetCodeManager 繼承 ObservableObject 且它的變數 problems 前面加上 @Published 是為了讓 SwiftUI 畫面取用資料且能夠同步更新。

這裡最重要的是,favoriteProblems 它就是我們自定義的 LeetCode 題目資料,需要存在手機本機,讓它再次打開 App 還能夠看到紀錄,所以前面加了 @AppStorage("favoriteProblems") 標記。

import SwiftUI

class LeetCodeManager: ObservableObject {
    @AppStorage("favoriteProblems") var favoriteProblems: Data?
    
    @Published var problems: [LeetCodeProblem] = [
        LeetCodeProblem(title: "Two Sum"),
        LeetCodeProblem(title: "Add Two Numbers"),
        LeetCodeProblem(title: "Longest Substring Without Repeating Characters"),
        // 加入更多題目...
    ]
    
    init() {
        if let data = favoriteProblems {
            if let decoded = try? JSONDecoder().decode([LeetCodeProblem].self, from: data) {
                problems = decoded
            }
        }
    }
    
    func toggleFavorite(for problem: LeetCodeProblem) {
        if let index = problems.firstIndex(where: { $0.id == problem.id }) {
            problems[index].isFavorited.toggle()
            saveFavorites()
        }
    }
    
    private func saveFavorites() {
        if let encoded = try? JSONEncoder().encode(problems) {
            favoriteProblems = encoded
        }
    }
}

再來我們的 SwiftUI 主畫面是顯示 LeetCode 題目列表,且可以讓使用者按收藏。

struct ContentView: View {
    @ObservedObject var leetCodeManager = LeetCodeManager()
    @State private var isSheetPresented = false
       
       var body: some View {
           NavigationView {
               List(leetCodeManager.problems) { problem in
                   HStack {
                       Text(problem.title)
                       Spacer()
                       Button(action: {
                           leetCodeManager.toggleFavorite(for: problem)
                       }) {
                           Image(systemName: problem.isFavorited ? "heart.fill" : "heart")
                       }
                   }
               }
               .navigationTitle("LeetCode Problems")
               .navigationBarItems(trailing: Button(action: {
                   // 打開Sheet
                   isSheetPresented.toggle()
    
               }) {
                   Image(systemName: "heart.fill")
               })
               .sheet(isPresented: $isSheetPresented) {
                   FavoriteProblemsView(leetCodeManager: leetCodeManager)
               }
           }
           
       }
}

@ObservedObject var leetCodeManager = LeetCodeManager() 表示leetCodeManager中的數據發生變化時,畫面會自動更新。

最後打開 Sheet 另外一個 View 頁面,將會顯示我們收藏實作過的 LeetCode 題目。

struct FavoriteProblemsView: View {
    @ObservedObject var leetCodeManager: LeetCodeManager
    @Environment(\.presentationMode) var presentationMode
    
    var body: some View {
        NavigationView {
            List(leetCodeManager.problems.filter { $0.isFavorited }) { problem in
                Text(problem.title)
            }
            .navigationTitle("Favorite Problems")
            .navigationBarItems(trailing: Button(action: {
                presentationMode.wrappedValue.dismiss()
            }) {
                Text("Close")
            })
        }
        
    }
}

這裡持續使用 @ObservedObject 而不用另外一個 @StateObject 的原因是因為需要讓兩頁兩邊都能監聽到 leetCodeManager 的資料變化所以才如此使用。

@Environment 這個操作符表示連動這個 Sheet 系統原生的狀態,宣告了之後可以拿這個設定的變數去關掉 Sheet,是一個很特別的做法。

模擬器頁面收藏效果如圖(好不容易錄製 Gif ,畫質好像被壓縮了…)但是可以很明確看到收藏頁面的題目有連動我們原本 LeetCode 題目列表按的收藏項目。

總結

在學 SwiftUI 的過程中發現很多東西都被大幅度的簡化,所以很多寫法沒辦法用以前寫程式的邏輯去想,有時候覺得不適應,但換個角度想,這樣的設計就是為了讓不懂寫程式的新手更好上手寫 App,只要知道怎麼調用語法就可以獲的想要的效果,不必學繁雜的程式碼運作,或許就是這套框架的精神,隨著一天一天學習過去,LeetCode SwiftUI App 知識也就越來越完整了,非常有成就感。


上一篇
Day 22: 導讀 LeetCode 演算法- Binary Search (Swift)
下一篇
Day 24: 導讀 LeetCode 演算法 - Graph 的 DFS 與 BFS (Swift)
系列文
用 SwiftUI 魔法變出 Leetcode 刷題知識學習 App!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言